package org.jflame.commons.file;

import org.jflame.commons.util.IOHelper;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.apache.commons.lang3.StringUtils;

/**
 * zip文件操作工具类
 * 
 * @author zyc
 */
public final class ZipHelper {

    /**
     * 压缩对文件或文件目录(使用默认压缩级别和utf-8编码)
     * 
     * @param srcFile 要压缩的文件
     * @param zipPath 压缩文件保存路径
     * @return 返回生成的压缩文件全路径
     * @throws Exception
     */
    public static String zip(final String srcFile, final String zipPath) throws Exception {
        return zip(srcFile, zipPath, null, StandardCharsets.UTF_8, null);
    }

    /**
     * 压缩对文件或文件目录(使用默认压缩级别和utf-8编码)
     * 
     * @param srcFile 要压缩的文件
     * @param zipPath 压缩文件保存路径
     * @param filter 文件过滤
     * @return 返回生成的压缩文件全路径
     * @throws Exception
     */
    public static String zip(final String srcFile, final String zipPath, FileFilter filter) throws Exception {
        return zip(srcFile, zipPath, null, StandardCharsets.UTF_8, filter);
    }

    /**
     * 压缩文件或文件目录
     * 
     * @param srcFile 要压缩的文件
     * @param zipPath 压缩文件保存路径
     * @param level 压缩级别0-9
     * @return 返回生成的压缩文件全路径
     * @throws Exception
     */
    public static String zip(final String srcFile, final String zipPath, int level) throws Exception {
        return zip(srcFile, zipPath, level, StandardCharsets.UTF_8, null);
    }

    /**
     * 压缩文件或文件目录
     * 
     * @param srcFile 要压缩的文件
     * @param zipPath 压缩文件保存路径
     * @param level 压缩级别0-9
     * @param charset 文件名字字符编码
     * @param filter 文件过滤
     * @return 返回生成的压缩文件全路径
     * @throws Exception
     */
    public static String zip(final String srcFile, final String zipPath, Integer level, final Charset charset,
            FileFilter filter) throws Exception {
        if (StringUtils.isEmpty(srcFile) || StringUtils.isEmpty(zipPath)) {
            throw new IllegalArgumentException("参数不能为空");
        }

        Path srcFilePath = Paths.get(srcFile);
        Path zipDirPath = Paths.get(zipPath);
        if (!Files.exists(srcFilePath)) {
            throw new FileNotFoundException(srcFile + "文件不存在");
        }
        // 如果压缩的是目录,判断压缩文件保存路径是否为源文件路径的子文件夹，防止无限递归压缩
        if (Files.isDirectory(srcFilePath) && zipDirPath.startsWith(srcFile)) {
            throw new IllegalArgumentException("保存目录不能与源文件相同或是其下级目录");
        }
        String zipFileName = StringUtils.substringBeforeLast(srcFilePath.getFileName()
                .toString(), ".") + ".zip";
        CheckedOutputStream cos = null;
        ZipOutputStream zipOutStream = null;
        BufferedOutputStream bufferOutStream = null;
        try {
            // 创建保存目录
            if (!Files.exists(zipDirPath) || !Files.isDirectory(zipDirPath)) {
                Files.createDirectories(zipDirPath);
            }

            // 创建压缩文件保存的文件对象
            Path zipFilePath = zipDirPath.resolve(zipFileName);
            if (Files.exists(zipFilePath)) {
                zipFilePath = Paths.get(zipPath, System.nanoTime() + "_" + zipFileName);
            }

            cos = new CheckedOutputStream(Files.newOutputStream(zipFilePath), new CRC32());
            zipOutStream = new ZipOutputStream(cos, charset == null ? StandardCharsets.UTF_8 : charset);
            if (level != null) {
                if (level < 0) {
                    level = Deflater.DEFAULT_COMPRESSION;
                }
                if (level > 9) {
                    level = 9;
                }
                zipOutStream.setLevel(level);
            }

            bufferOutStream = new BufferedOutputStream(zipOutStream);
            zip(null, srcFilePath.toFile(), zipOutStream, bufferOutStream, filter);

            return zipFilePath.toString();
        } finally {
            IOHelper.closeQuietly(bufferOutStream);
        }
    }

    /**
     * 解压文件,使用UTF-8编码
     * 
     * @param unzipDir 解压到目录
     * @param zipFile 要解压的文件
     * @throws IOException
     */
    public static void unzip(final String unzipDir, final String zipFile) throws IOException {
        unzip(unzipDir, zipFile, StandardCharsets.UTF_8);
    }

    /**
     * 解压文件
     * 
     * @param unzipDir 解压到目录
     * @param zipFile 要解压的文件
     * @throws IOException
     */
    public static void unzip(final String unzipDir, final String zipFile, final Charset charset) throws IOException {
        File unzipDirFile = new File(unzipDir);
        if (!unzipDirFile.exists()) {
            unzipDirFile.mkdirs();
        }
        try (InputStream in = new BufferedInputStream(new FileInputStream(zipFile))) {
            unzip(unzipDirFile, in, charset);
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * 递归压缩文件夹
     * 
     * @param srcRootDir 压缩文件夹根目录的子路径
     * @param file 当前递归压缩的文件或目录对象
     * @param zos 压缩文件存储对象
     * @param filter 文件过滤
     * @throws IOException
     */
    private static void zip(String name, final File file, final ZipOutputStream zos,
            BufferedOutputStream bufferOutStream, FileFilter filter) throws IOException {
        if (name == null) {
            name = file.getName();
        }
        if (filter != null && !filter.accept(file)) {
            return;
        }
        // 如果是文件，则直接压缩该文件
        if (file.isDirectory()) {
            // 如果是目录，则压缩整个目录. 压缩目录中的文件或子目录
            File[] childFileList = file.listFiles();
            if (childFileList.length == 0) {
                zos.putNextEntry(new ZipEntry(name + "/"));
            } else {
                for (int n = 0; n < childFileList.length; n++) {
                    zip(name + "/" + childFileList[n].getName(), childFileList[n], zos, bufferOutStream, filter);
                }
            }

        } else {
            int len;
            byte[] byteBuf = new byte[2048];

            zos.putNextEntry(new ZipEntry(name));
            try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
                while ((len = bis.read(byteBuf)) != -1) {
                    bufferOutStream.write(byteBuf, 0, len);
                }
                bufferOutStream.flush();
            }
            zos.closeEntry();
        }
    }

    /**
     * 将输入的压缩文件流解压到解压目录(unzipDir)
     * 
     * @param unzipDir 解压目录
     * @param in 压缩文件流
     * @throws IOException
     */
    private static void unzip(final File unzipDir, final InputStream in, Charset charset) throws IOException {
        ZipEntry entry = null;
        try (ZipInputStream zin = new ZipInputStream(in, charset == null ? StandardCharsets.UTF_8 : charset);) {
            while ((entry = zin.getNextEntry()) != null) {
                File path = new File(unzipDir, entry.getName());
                if (entry.isDirectory()) {
                    path.mkdirs();
                } else {
                    File parentFile = path.getParentFile();
                    if (parentFile != null && !parentFile.equals(path.getAbsoluteFile())) {
                        parentFile.mkdirs();
                    }

                    try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(path))) {
                        byte[] buf = new byte[1024];
                        int n = 0;
                        while ((n = zin.read(buf)) != -1) {
                            bos.write(buf, 0, n);
                        }
                        bos.flush();
                    } catch (IOException e) {
                        throw e;
                    }
                }
            }
        } catch (IOException e) {
            throw e;
        }
    }

}